feat: add SceneGraph assertion commands#2
Closed
altaywtf wants to merge 4 commits into
Closed
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds generic SceneGraph query/assert/wait primitives to rokit so downstream app repos can build proof loops against Roku runtime state without copying XML parsing and assertion helpers.
Changes:
- Add new CLI commands:
sgnodes,assert-node,wait-node, andwait-active. - Introduce SceneGraph XML helpers (
readNamedNodeAttribute, visibility checks, and assertion helpers). - Make
active-appXML parsing tolerant of<app>nodes with no attributes, with accompanying tests.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| test/xml.test.ts | Adds coverage for active-app parsing with/without <app> attributes. |
| test/scenegraph.test.ts | Adds unit tests for named-node attribute reads, visibility, and assertion error messages. |
| test/cli.test.ts | Extends CLI help output test to include the new wait-node command. |
| src/xml.ts | Updates readActiveApp regex to handle <app> tags without attributes. |
| src/scenegraph.ts | Adds SceneGraph XML parsing + assertion helpers used by CLI and Roku query wrappers. |
| src/roku.ts | Adds SceneGraph query/wait wrappers and exports waitForActiveApp. |
| src/cli.ts | Wires new CLI commands, argument parsing, and help output for SceneGraph assertions/waits. |
| README.md | Documents new SceneGraph-related CLI commands and their intended boundary. |
| AGENTS.md | Updates project guidance to keep SceneGraph helpers generic and app contracts in app repos. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return undefined; | ||
| } | ||
|
|
||
| return readXmlAttribute(attributes, attributeName); |
|
|
||
| export const readNamedNodeAttributes = (xml: string, nodeName: string): string | undefined => { | ||
| const pattern = new RegExp( | ||
| `<[A-Za-z0-9]+\\b(?=[^>]*\\bname="${escapeRegExp(nodeName)}")([^>]*)>`, |
Comment on lines
+37
to
+41
| export const isNamedNodeVisible = (xml: string, nodeName: string): boolean => { | ||
| const attributes = readNamedNodeAttributes(xml, nodeName); | ||
|
|
||
| return attributes !== undefined && !attributes.includes('visible="false"'); | ||
| }; |
| } | ||
|
|
||
| if (!attributes) { | ||
| throw new Error(`expected SceneGraph node "${nodeName}"`); |
Comment on lines
+253
to
+269
| const parseNodeCondition = (commandName: string, args: readonly string[]): NodeCondition => { | ||
| const [nodeName, condition, ...rest] = args; | ||
|
|
||
| if (!nodeName || !condition) { | ||
| return fail( | ||
| `usage: rokit ${commandName} <node-name> <visible|hidden|absent|text|attr> [value] [--timeout-ms <ms>]`, | ||
| ); | ||
| } | ||
|
|
||
| if (condition === "visible" || condition === "hidden" || condition === "absent") { | ||
| const timeoutMs = parseTimeoutOption(rest, `rokit ${commandName} <node-name> ${condition}`); | ||
| return { | ||
| expectation: { state: condition }, | ||
| nodeName, | ||
| timeoutMs, | ||
| }; | ||
| } |
Comment on lines
+385
to
+386
| rokit assert-node <node-name> <visible|hidden|absent|text|attr> [value] | ||
| rokit wait-node <node-name> <visible|hidden|absent|text|attr> [value] [--timeout-ms <ms>] |
Comment on lines
171
to
+229
| const parseCommand = (argv: readonly string[]): Command => { | ||
| const [name, ...args] = argv; | ||
|
|
||
| if (name === "check") { | ||
| return { name }; | ||
| } | ||
|
|
||
| if (name === "device-info") { | ||
| return { name }; | ||
| } | ||
|
|
||
| if (name === "active-app") { | ||
| return { name }; | ||
| } | ||
|
|
||
| if (name === "wait-active") { | ||
| const appId = args[0]; | ||
|
|
||
| if (!appId) { | ||
| fail("usage: rokit wait-active <app-id> [--timeout-ms <ms>]"); | ||
| } | ||
|
|
||
| return { | ||
| appId, | ||
| name, | ||
| timeoutMs: parseTimeoutOption(args.slice(1), `rokit ${name} <app-id>`), | ||
| }; | ||
| } | ||
|
|
||
| if (name === "launch") { | ||
| return { name, args: parseLaunchArgs(args) }; | ||
| } | ||
|
|
||
| if (name === "press") { | ||
| if (args.length === 0) { | ||
| fail("usage: rokit press <key> [key...]"); | ||
| } | ||
|
|
||
| return { name, keys: args }; | ||
| } | ||
|
|
||
| if (name === "query") { | ||
| const path = args[0]; | ||
|
|
||
| if (!path) { | ||
| fail("usage: rokit query <ecp-path>"); | ||
| } | ||
|
|
||
| return { name, path }; | ||
| } | ||
|
|
||
| if (name === "sgnodes") { | ||
| return { name }; | ||
| } | ||
|
|
||
| if (name === "assert-node" || name === "wait-node") { | ||
| return { name, args: parseNodeCondition(name, args) }; | ||
| } | ||
|
|
Comment on lines
+52
to
+58
|
|
||
| if (!target) { | ||
| throw new Error("ROKIT_TARGET is not set"); | ||
| } | ||
|
|
||
| const context: RokuContext = { | ||
| target, |
Member
Author
|
Closing for now; continuing iteration on the branch without PR ceremony. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds generic SceneGraph and remote-control runtime primitives to
rokit, then rounds out the repo docs, delivery contract, and agent-readiness setup so app repos can build proof loops without copying Roku XML parsing/assertion helpers.Changed
rokit sgnodesfor raw SceneGraph outputrokit assert-nodefor one-shot named-node checksrokit wait-nodefor polling named-node conditionsrokit wait-activefor foreground app waitsrokit press --delay-ms <ms> ...for stable remote navigation sequences@putdotio/rokitfor app-specific scenario scriptsdocs/DISTRIBUTION.md,docs/READINESS.md,CLAUDE.md -> AGENTS.md, PR/issue templates, Dependabot, and an optional pre-push hookRisks
Low-medium. SceneGraph query support depends on the device/app exposing
/query/sgnodes/all; app repos should still own product-specific assertions and screenshots.Verification
pnpm verifyROKIT_TARGET=<configured> pnpm live:smokewith command output redirectedCLAUDE.mdis a symlink toAGENTS.md.env.exampleremains tracked while local env files are ignoredgit diff --checkCloses #1 partially; product-specific player scenarios still belong in
putio-roku.